package wortubung.core;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import wortubung.WortUbungException;
import wortubung.wordlist.WordListInfo;

import static wortubung.util.CommonUtils.*;

import static java.lang.Math.max;

public class WORD implements Item, Comparable<WORD> {
    private LANG    lang;
    private POSInfo defaultPosInfo;
    private String  word;
    private String  alias;
    private String  inflection;
    private String  pronounce;
    private List<MEANING> definition = new ArrayList<MEANING>();
    private List<FixedUsage> fixedUsages;
    private List<WORD> derivation;
    private int     level;
    private WordListInfo wordListInfo;

    private static final Set<String> excludeList;   // "small" words not to be checked against answer

    private transient WORD parent;                  // if this is a derived word
    private transient String anglicized;            // useful for testing
    private transient String allPOSes;              // useful for simple listing and testing
    private transient String withAlias;             // useful for testing
    private transient String withAliasPOS;          // useful for testing
    private transient String digest;                // useful for reverse testing
    private transient List<String> cleansedAnswers; // parenthesized contents removed
    private transient Object temp;

    static {
        excludeList = new HashSet<String>();
        String[] list = {
        	"a", "about", "against", "and", "an", "any", "are", "as", "at",
        	"be", "beyond", "but", "by", "do", "down", "for", "from",
        	"has", "have", "here", "in", "into", "is", "it",
        	"of", "one", "on", "onto", "or", "o.s.", "out", "over",
        	"sb", "some", "sth",
            "that", "the", "there", "they", "this", "to", "towards",
            "up", "under", "with", "yet"
        };
        for (String s : list)
            excludeList.add(s);
    }

    public WORD(WordListInfo listInfo, String word, String alias, POSInfo posInfo, LANG lang) {
    	this(listInfo, word, alias, (String)null, lang);
    	setPOSInfo(posInfo);
    }

    public WORD(WordListInfo listInfo, String word, String _pos, LANG lang) {
    	this.wordListInfo = listInfo;
        if (listInfo != null)
	    listInfo.addWord(this);

        this.level = 0;
    	this.word = word;
    	this.lang = lang;
        try {
            if (isNotEmpty(_pos))
            	setPOS(_pos);
    	} catch(Exception e) {
    		if (e instanceof WortUbungException)
    			throw (WortUbungException)e;
            throw new WortUbungException(String.format("%s [%s] Word: '%s' has no POS set.", e.getMessage(), wordListInfo.getShortName(), word));
    	}
    }

    public WORD(WordListInfo listInfo, String word, String alias, String pos, LANG lang) {
        this(listInfo, word, pos, lang);
        this.alias = alias;
    }

    public void check() {
		if (defaultPosInfo == null)
			throw new WortUbungException(String.format("[%s] Word: '%s' has no default POS set.", wordListInfo.getShortName(), word));
    }
    public String getWord() { return word; }
    public String getAlias()  { return alias; }
    public boolean hasAlias() { return isNotEmpty(alias); }
    public POSInfo getDefaultPOSInfo()  {
    	if (defaultPosInfo == null)
    		defaultPosInfo = definition.get(0).getPOSInfo();
    	return defaultPosInfo;
    }
    @Override
    public POS getDefaultPOS()  { return defaultPosInfo.getPOS(); }
    public NounCategory getNounCategory() { return isNoun() ? defaultPosInfo.getNounCategory() : null; }
    public String getDefaultPOSDisplay(boolean full) { return getDefaultPOSInfo().toDisplay(full); }
    public LANG   getLang() { return lang; }
    public WordListInfo getWordListInfo() { return wordListInfo; }

    public String getInflection() { return inflection; }
    public String getPronounce() { return pronounce; }
    public int    getLevel() { return level; }
    public boolean isDerived() { return parent != null; }
    public WORD getParent() { return parent; }

    public List<MEANING> getDefinition() { return definition; }
    public int numMeanings() { return max(1, length(definition)) + numFixedUsages(); }
    public List<WORD> getDerivation() { return derivation; }
    public List<FixedUsage> getFixedUsages() { return fixedUsages; }
    public POSCategory getPOSCategory() { return getDefaultPOS().getCategory(); }
    public boolean isNoun() { return getPOSCategory() == POSCategory.Noun; }
    public boolean isVerb() { return getPOSCategory() == POSCategory.Verb; }
    public boolean isReflexiveVerb() { return getDefaultPOS().isReflexiveVerb(); }

    public void setInflection(String inflection) { this.inflection = inflection; }
    public void setPronounce(String pronounce) { this.pronounce = pronounce; }
    public void setLevel(int level) { this.level = level; }
    public void setDerived(WORD parent) { this.parent = parent; }
    public void setPOS(String posStr) { setPOSInfo(POSInfo.parsePOS(posStr)); }
    public void setPOSInfo(POSInfo posInfo) { this.defaultPosInfo = posInfo; }

    public int numMeaningsCJK() {
    	int cnt = 0;
    	if (definition != null) {
    		for (MEANING def : definition)
	    		if (def.hasCJK())
	    			++cnt;
    	}
    	if (fixedUsages != null) {
	    	for (FixedUsage fu : fixedUsages)
	    		if (fu.getMeaning().hasCJK())
	    			++cnt;
    	}
    	return cnt;
    }

    public int numFixedUsages() { return length(fixedUsages); }
    public int numFixedUsages(POS type) {
    	if (fixedUsages == null)
    		return 0;
    	int cnt = 0;
    	for (FixedUsage fu : fixedUsages)
    		if (fu.getDefaultPOS() == type)
    			++cnt;
    	return cnt;
    }
    public int numIdioms() { return numFixedUsages(POS.IDIOM); }
    public int numSlangs() { return numFixedUsages(POS.SLANG); }
    public int numLocutions() { return numFixedUsages(POS.LOCUTION); }

    public MEANING getDefinitionForTest() {
    	int sz = definition.size();
    	if (sz == 0)
    		return null;
    	return definition.get((sz==1) ? 0 : random.nextInt(sz));
    }

    public int numExamples() {
    	int cnt = 0;
    	for (MEANING m : definition)
    		cnt += m.numExamples();
    	return cnt;
    }

    /**
     * @return { noun, verb, adj, adv, conj/prep/pron, excl, fixed-usage }
     */
    public boolean[] getMeaningTypes() {
    	boolean[] ret = new boolean[7];
    	for (MEANING def : definition) {
    		if (def.isNoun())             ret[0] = true;
    		else if (def.isVerb())        ret[1] = true;
    		else if (def.isAdjective())   ret[2] = true;
    		else if (def.isAdverb())      ret[3] = true;
    		else if (def.isSmallWord())   ret[4] = true;
    		else if (def.isExclamation()) ret[5] = true;
    		else if (def.isFixedUsage())  ret[6] = true;
    	}
    	return ret;
    }

    public void setTemp(Object temp) { this.temp = temp; }
    public Object getTemp() { return temp; }

    public String getWordWithAlias() {
        return getWordWithAlias(false);
    }

    public String getWordWithAlias(boolean withPOS) {
        if (withAlias == null || withAliasPOS == null) {
    	    String ret = this.word;
            if (isReflexiveVerb())
            	ret = lang.getReflexiveVerbDisplay(word, false);

    	    if (isEmpty(alias)) {
                withAlias = withAliasPOS = ret;
            } else {
    	        ret += " [" + alias + "]";
                withAlias = ret;
                if (withPOS)
    	            ret += "/" + getDefaultPOSDisplay(false) + "/";
                withAliasPOS = ret;
            }
        }
        return withPOS ? withAliasPOS : withAlias;
    }

    public boolean searchFirstDefinition(String regex) {
    	for (MEANING m : definition) {
    		if (m.matches(regex)) {
    			setTemp(m);
    			return true;
    		}
    	}
    	if (fixedUsages != null) {
        	for (FixedUsage fu : fixedUsages) {
        		if (fu.getMeaning().matches(regex)) {
        			setTemp(fu);
        			return true;
        		}
        	}
    	}
    	return false;
    }

    @Override
    public void addDefinition(MEANING def) {
        if (this.defaultPosInfo == null)
            this.defaultPosInfo = def.getPOSInfo();
        definition.add(def);
    }

    public void addFixedUsage(FixedUsage fixedUsage) {
        if (fixedUsages == null)
        	fixedUsages = new ArrayList<FixedUsage>();
        fixedUsages.add(fixedUsage);
    }

    public void addDerivedWord(WORD derived) {
        derived.setDerived(this);
        if (derivation == null)
            derivation = new ArrayList<WORD>();
        derivation.add(derived);
    }

    public String getDigest() {
        if (digest == null)
            digest = Integer.toHexString((getWordWithAlias() + '/' + getDefaultPOSDisplay(false)).hashCode());
        return digest;
    }

    public String getAllPOSes() {
        if (allPOSes == null) {
            if (isEmpty(getDefinition())) {
                allPOSes = getDefaultPOSDisplay(false);
            } else {
                boolean hasMore = false;
                StringBuilder sb = new StringBuilder();
                HashSet<String> set = new HashSet<String>();
                for (MEANING m : getDefinition()) {
                    String x = m.getPOSDisplay(false);
                    if (set.contains(x))
                        continue;
                    set.add(x);

                    if (hasMore)
                        sb.append(',');
                    else
                        hasMore = true;
                    sb.append(x);
                }
                allPOSes = sb.toString();
            }
        }
        return allPOSes;
    }

    private String getDefinitionsDisplay(MEANING meaning, boolean isPOSFullName) {
        int len = getDefinition().size();
        if (len == 0)
            return "/" + getDefaultPOSDisplay(isPOSFullName) + "/";

        if (meaning != null)
        	return meaning.getMeaningDisplay(isPOSFullName);

        StringBuilder sb = new StringBuilder();
        for (int i=0; i<len; ++i) {
            if (i>0)
                sb.append("  $");
            sb.append(getDefinition().get(i).getMeaningDisplay(isPOSFullName));
        }
        return sb.toString();

    }

    public int getMaxMeaningLength() {
        int maxLen = 0;
        for (MEANING m : definition) {
            int len = m.getMeaningFull().length();
            if (len > maxLen)
                maxLen = len;
        }
        return maxLen;
    }

    public String getAnglicizedWord() {
        if (anglicized == null)
            anglicized = lang.toLatin(word, false);
        if (anglicized.equals(word))
            anglicized = word;
        return anglicized;
    }

//    public boolean checkConjugation(String line) {
//        if (getPOSCategory() != POSCategory.Verb || isEmpty(conjugation))
//            return true;
//
//        String[] input = separate(line, ",");
//        try {
//            for (int i=0; i<conjugation.length; ++i) {
//                if (!LatinScript.deAccent(conjugation[i]).equals(input[i]))
//                    return false;
//            }
//            return true;
//        } catch(Exception e) {
//            return false;
//        }
//    }

    public String[] toQuickLine(int extraLen, String prefix, int wordWidth, int pageWidth, boolean isPOSFullName) {
        return toQuickLine(extraLen, prefix, wordWidth, pageWidth, null, isPOSFullName);
    }

    public String[] toQuickLine(int extraLen, String prefix, int wordWidth, int pageWidth, MEANING meaning, boolean isPOSFullName) {
    	StringBuilder sb = new StringBuilder();
    	append(sb, prefix, makeStringLen(getWordWithAlias(), 2, wordWidth, '.'), isDerived() ? " *- " : "  - ");
    	String emptyPrefix = makeLine(' ', extraLen + sb.length());
    	int meaningWidth = pageWidth - emptyPrefix.length();
        List<String> lines = wrapText(getDefinitionsDisplay(meaning, isPOSFullName), meaningWidth, null);
        if (lines.size() > 1 && numMeanings() > 1) {
        	// We show each meaning on a separate line since this is multi-line anyway.
        	lines.clear();
        	for (MEANING def : getDefinition())
                wrapText(def.getMeaningDisplay(isPOSFullName), meaningWidth, lines);
        }
		boolean first = true;
		for (String ln : lines) {
			if (first) {
				first = false;
				sb.append(ln);
			} else {
				sb.append('\n').append(emptyPrefix).append(ln);
			}
		}
        return sb.toString().split("\n");
    }

    public String toSimpleDisplay(boolean isPOSFullName) {
    	String display;
        if (definition.size() == 0)
        	display = "/" + getDefaultPOSDisplay(isPOSFullName) + "/";
        else
        	display = definition.get(0).getMeaningDisplay(isPOSFullName);
        return getWordWithAlias() + ' ' + display;
    }

    public int compareTo(WORD toWord) {
        int ret = compareIgnoreCase(getAnglicizedWord(), toWord.getAnglicizedWord());
        if (ret != 0)
        	return ret;

        if (alias != null && toWord.alias != null) {
            ret = compareIgnoreCase(alias.toLowerCase(), toWord.alias.toLowerCase());
            if (ret != 0)
            	return ret;
        }

        return compare(toWord.isReflexiveVerb(), isReflexiveVerb());
    }

    public String[] toCompleteInfoDisplay(int pageWidth, boolean isPOSFullName) {
        return toCompleteInfoDisplay(null, pageWidth, isPOSFullName);
    }

    public String[] toCompleteInfoDisplay(String prefix, int pageWidth, boolean isPOSFullName) {
    	String emptyPrefix = makeLine(' ', length(prefix));

    	StringBuilder sb = new StringBuilder();
    	append(sb, prefix, getWordWithAlias(), ' ');
    	appendWithPrefixSuffix(sb, " [", pronounce, "]");
        if (isNotEmpty(inflection))
            appendWithPrefixSuffix(sb, " {", inflection, "}");
    	if (definition.size() <= 1 && numFixedUsages() == 0) {
			append(sb, " /", getDefaultPOSDisplay(isPOSFullName), "/");
   			if (definition.size() == 0)
   				return sb.toString().split("\n");
    		if (definition.size() == 1 && isEmpty(derivation) && !isDerived()) {
    			MEANING m =  definition.get(0);
    			if (m.numExamples() == 0) {
    				String meaning = m.getMeaningFull();
    				if (sb.length() + meaning.length() + 1 <= pageWidth)
    					return sb.append(' ').append(meaning).toString().split("\n");
    			}
    		}
    	}
        if (isNotEmpty(derivation)) {
        	int started = 0;
       	    for (WORD w : derivation)
           		sb.append((started++ == 0) ? "  [==> " : ", ").append(w.getWordWithAlias());
       	    if (started > 0)
       	    	sb.append(']');
    	}
        if (isDerived())
        	append(sb, "  [<== ", parent.getWordWithAlias(), "]");

        String preamble;
        String preambleRest;
    	String firstLineFmt;
    	String examplePreamble;
    	String examplePreambleRest;
    	boolean onlyOne = numMeanings() + numFixedUsages() == 1;
    	if (onlyOne) {
        	firstLineFmt        = null;
    		preamble            =
    		preambleRest        = String.format("\n%s  ", emptyPrefix);
        	examplePreamble     = String.format("\n%s    [Ex] ", emptyPrefix);
        	examplePreambleRest = String.format("\n%s         ", emptyPrefix);
    	} else {
        	firstLineFmt        = "\n%s  %2d. ";
            preamble            = null;
    		preambleRest        = String.format("\n%s      ",  emptyPrefix);
        	examplePreamble     = String.format("\n%s        [Ex] ", emptyPrefix);
        	examplePreambleRest = String.format("\n%s             ", emptyPrefix);
    	}
    	
    	int idx = 1;
        for (MEANING meaning : definition)
        	meaningDisplay(sb, idx++, "", meaning, pageWidth, onlyOne, firstLineFmt, emptyPrefix,
            		preamble, preambleRest, examplePreamble, examplePreambleRest);

        if (fixedUsages != null) {
            for (FixedUsage fu : fixedUsages) {
            	prefix = "{" + fu.getDefaultPOS().getShortName() + "} " + fu.getExpression() + ": ";
            	meaningDisplay(sb, idx++, prefix, fu.getMeaning(), pageWidth, onlyOne, firstLineFmt, emptyPrefix,
                		preamble, preambleRest, examplePreamble, examplePreambleRest);
            }
        }

        sb.append("\n");
        return sb.toString().split("\n");
    }

    private void meaningDisplay(StringBuilder sb, int idx, String prefix, MEANING meaning,
    		int pageWidth, boolean onlyOne, String firstLineFmt, String emptyPrefix,
    		String preamble, String preambleRest, String examplePreamble, String examplePreambleRest)
    {
        List<String> lines = new ArrayList<String>();
    	if (onlyOne) {
    		wrapText(prefix + meaning.getMeaningFull(), pageWidth - preamble.length(), lines);
    		for (String ln : lines)
    			sb.append(preamble).append(ln);
    	} else {
    		preamble = String.format(firstLineFmt, emptyPrefix, idx);
    		wrapText(prefix + meaning.getMeaningDisplay(false), pageWidth - preamble.length(), lines);
    		boolean first = true;
    		for (String ln : lines) {
    			if (first) {
    				first = false;
    				sb.append(preamble).append(ln);
    			} else {
    				sb.append(preambleRest).append(ln);
    			}
    		}
    	}

	    if (meaning.numExamples() > 0) {
            for (EXAMPLE ex : meaning.getExamples()) {
            	String text = ex.getExampleDisplay();
            	if (ex.getExplanation() != null)
            		text += "  [" + ex.getExplanation() + "]";
            	lines.clear();
        		wrapText(text, pageWidth - examplePreamble.length(), lines);
        		boolean first = true;
        		for (String ln : lines) {
        			if (first) {
        				first = false;
        				sb.append(examplePreamble).append(ln);
        			} else {
        				sb.append(examplePreambleRest).append(ln);
        			}
        		}
            }
        }
    }

    public boolean checkAnswer(TestLine testLine) {
        final String NO_ANSWER = "No answer is entered.";
        final String ANSWER_WRONG = "Answer is wrong.";
        final String NO_NOUN_GENDER = "Noun gender is not answered.";
        final String NOUN_GENDER_WRONG = "Noun gender is wrong.";
        final String NO_NOUN_PLURAL = "Noun plural is not answered.";
        final String NOUN_PLURAL_WRONG = "Noun plural is wrong.";
        final String VERB_CONJ_WRONG = "Verb conjugation is wrong.";

        if (isEmpty(testLine.getAnswerText()))
            return testLine.fail(NO_ANSWER);

        String preError = null;
        if (isNoun()) {
        	if (getLang().hasNounGender()) {
        		try {
        			if (testLine.getPOS() == null)
        				preError = NO_NOUN_GENDER;
        			else if (testLine.getPOS() != getDefaultPOS())
        				preError = NOUN_GENDER_WRONG;
        		} catch(Exception e) {
        			preError = NOUN_GENDER_WRONG;
        		}
        	}
        	if (getLang().isIrregularPlural()) {
                String answerPlural = testLine.getPlural();
                if (isEmpty(answerPlural))
                	preError = NO_NOUN_PLURAL;
                else if (!answerPlural.equals(getInflection()))
                	preError = NOUN_PLURAL_WRONG;
    	    }
    	}

//        if (isVerb() && !checkConjugation(testLine.getConjugation()))
//            preError = VERB_CONJ_WRONG;

        List<String> answers = getCleansedAnswers();
        if (isEmpty(answers)) {
            if (isNotEmpty(preError))
                return testLine.fail(preError);
            return true;
        }

        boolean good = false;
        String[] answerParts = testLine.getAnswerText().replace(",", " ").replace(".", " ").toLowerCase().split(" +");
        trim(answerParts);
loop:   for (String part : answerParts) {
            if (!excludeList.contains(part)) {
                for (String ans : answers) {
                    if (ans.contains(part)) {
                        good = true;
                        break loop;
                    }
                }
            }
		}
        if (!good)
        	return testLine.fail(preError, ANSWER_WRONG);
        if (isNotEmpty(preError))
        	return testLine.fail(preError);
        return true;
    }

    private List<String> getCleansedAnswers() {
        if (cleansedAnswers == null) {
            cleansedAnswers = new ArrayList<String>();
            boolean allEmpty = true;
            for (MEANING m : getDefinition()) {
                String s = m.getMeaning().toLowerCase();
                if (isNotEmpty(s))
                    allEmpty = false;
                int idx = s.indexOf('(');
                while (idx >= 0) {
                    int idx1 = s.indexOf(')', idx);
                    if (idx1 < 0) {
                        idx = -1;
                        break;
                    } else {
                        s = s.substring(0, idx) + s.substring(idx1+1);
                        idx = s.indexOf('(');
                    }
                }
                cleansedAnswers.add(s);
            }
            if (allEmpty)
                cleansedAnswers.clear();
        }
        return cleansedAnswers;
    }
}
